package ElectricCraft.viewers;

import ElectricCraft.drawer.Drawer;
import ElectricCraft.model.Orientation;
import ElectricCraft.model.ChipElement;
import ElectricCraft.model.ChipModel;

import java.awt.*;
import java.util.LinkedList;
import java.util.List;

/**
 * ChipEditor class implements editing routines.
 * Allows to edit Chip Diagrams.
 */
public class ChipEditor extends ChipViewer {
	public ChipEditor() {
		newItems = new ChipElement[newItemsRange.width][];
		for (int k = 0; k < newItemsRange.width; ++k) {
			newItems[k] = new ChipElement[newItemsRange.height];
			for (int kkk = 0; kkk < newItemsRange.height; ++kkk) {
				newItems[k][kkk] = new ChipElement();
			}
		}

		newItems[0][0].initKey(true,  Orientation.WEST_EAST);
		newItems[0][1].initKey(false, Orientation.WEST_EAST);
        newItems[1][0].initKey(true,  Orientation.NORTH_SOUTH);
		newItems[1][1].initKey(false, Orientation.NORTH_SOUTH);

		newItems[2][0].initDiode(Orientation.EAST);
		newItems[2][1].initDiode(Orientation.WEST);
		newItems[3][0].initDiode(Orientation.NORTH);
		newItems[3][1].initDiode(Orientation.SOUTH);

		newItems[0][2].initPower();
		newItems[1][2].initInput(0);
		newItems[2][2].initOutput(0);
        newItems[3][2].initChip(-1, 0, 0, false);

        // TODO: cross produce fake connections, should be converted to single wire (left-right) when has non-wire neighbour
	}

	protected static class EditorItem {
		public final boolean allow[];

        private boolean [] allowByDirs(int dirMask) {
            boolean [] res = new boolean[] { false, false, false, false };
            for (int k = 0; k < 4; ++k) {
                if (Orientation.checkDir(dirMask, k)) {
                    res[k] = true;
                }
            }

            return res;
        }

		public EditorItem(ChipElement elem) {
			switch (elem.type) {
				case ChipElement.ELEMENT_WIRE:
					if (elem.itemType == Drawer.WIRE_CROSS) {
						allow = allowByDirs(Orientation.EVERYWHERE_MASK);
						return ;
					}

					allow = allowByDirs(elem.itemType);
					break;
				case ChipElement.ELEMENT_DIODE:
					if (elem.orientation % 2 == 0) {
						allow = allowByDirs(Orientation.NORTH_SOUTH_MASK);
					} else {
						allow = allowByDirs(Orientation.WEST_EAST_MASK);
					}
					break;

                case ChipElement.ELEMENT_POWER:
				case ChipElement.ELEMENT_KEY:       allow = allowByDirs(Orientation.EVERYWHERE_MASK); break;

				case ChipElement.ELEMENT_INPUT:     allow = allowByDirs(Orientation.EAST_MASK); break;
				case ChipElement.ELEMENT_OUTPUT:    allow = allowByDirs(Orientation.WEST_MASK); break;

				case ChipElement.ELEMENT_MARK:      allow = allowByDirs(0); break;

                case ChipElement.ELEMENT_CHIP_PART:
                    if (Orientation.checkMask(elem.orientation, Drawer.CHIP_OUTPUT_BIT)) {
                        allow = allowByDirs(Orientation.EAST_MASK);
                    } else {
                        allow = allowByDirs(Orientation.WEST_MASK);
                    }
                    break;

				default:
					throw new RuntimeException("Invalid element type: " + elem.type);
			}
		}
	}

    private final List<NotificationListener> changeNotifiers = new LinkedList<NotificationListener>();
	public void addChangeNotificationListener(NotificationListener notifier) {
		changeNotifiers.add(notifier);
	}

	private void notifyChanged() {
		for (NotificationListener notifier : changeNotifiers) {
			notifier.notified();
		}

        updateView();
	}

	protected EditorItem items[][];
	public void setModel(ChipModel model) {
		super.setModel(model);

		items = new EditorItem[model.elements.length][];
		for (int k = 0; k < items.length; ++k) {
			items[k] = new EditorItem[model.elements.length];

			for (int index = 0; index < items[k].length; ++index) {
				ChipElement elem = model.elements[k][index];

				if (elem == null) {
					continue;
				}

				items[k][index] = new EditorItem(elem);
			}
		}
	}

	private boolean wireDrawStarted = false;
	private final Point prevIndex = new Point();
	public void startWireDraw(int x, int y) {
		if (newElementMode) {
			return ;
		}

		if (!getElementByPos(x, y, elementIndex)) {
			return ;
		}

		ChipElement elem = model.elements[elementIndex.x][elementIndex.y];

		wireDrawStarted = true;
		prevIndex.setLocation(elementIndex);

		if (elem == null) {
			createWire(elementIndex.x, elementIndex.y);

            notifyChanged();
		}
	}

	public boolean isWireDrawStarted() {
		return wireDrawStarted;
	}

	public void stopWireDraw() {
		wireDrawStarted = false;
	}

	private boolean isWireOrAllow(Point index, int dir) {
		ChipElement elem = model.elements[index.x][index.y];
		if (elem != null && elem.type == ChipElement.ELEMENT_WIRE) {
			return true;
		}

		EditorItem item = items[index.x][index.y];

		return item != null && item.allow[dir];
	}

	public void updateWireDraw(int x, int y) {
		if (wireDrawStarted && !getElementByPos(x, y, elementIndex)) {
			return ;
		}

		int dir = Orientation.getDirByNeighbours(prevIndex, elementIndex);
		int invDir = Orientation.invertDir(dir);

		ChipElement elem = model.elements[elementIndex.x][elementIndex.y];
		EditorItem item = items[elementIndex.x][elementIndex.y];

		ChipElement prevElem = null;
		EditorItem prevItem = null;
		if (dir != Orientation.UNKNOWN) {
			prevElem = model.elements[prevIndex.x][prevIndex.y];
			prevItem = items[prevIndex.x][prevIndex.y];
		}

		if (elem == null) {
			elem = createWire(elementIndex.x, elementIndex.y);
			item = items[elementIndex.x][elementIndex.y];

			if (dir != Orientation.UNKNOWN && isWireOrAllow(prevIndex, dir)) {
				item.allow[invDir] = true;
				elem.itemType = Drawer.getWireTypeByDirs(item.allow);
			}
		} else if (dir != Orientation.UNKNOWN && elem.type == ChipElement.ELEMENT_WIRE && !item.allow[invDir]) {
			if (prevElem.type == ChipElement.ELEMENT_WIRE || prevItem.allow[dir]) {
				// check whether we should create cross
				int sideDir = Orientation.rotateDir(dir, true);
				int invSideDir = Orientation.rotateDir(dir, false);

				if (!item.allow[dir] && !item.allow[invDir] && item.allow[sideDir] && item.allow[invSideDir]) {
					item.allow[dir] = item.allow[invDir] = true;
					elem.itemType = Drawer.WIRE_CROSS;
				} else {
					item.allow[invDir] = true;
					elem.itemType = Drawer.getWireTypeByDirs(item.allow);
				}
			}
		}

		// update previous item
		if (dir != Orientation.UNKNOWN && item.allow[invDir]) {
			if (prevElem.type == ChipElement.ELEMENT_WIRE && !prevItem.allow[dir]) {
				prevItem.allow[dir] = true;
				prevElem.itemType = Drawer.getWireTypeByDirs(prevItem.allow);
			}
		}

		prevIndex.setLocation(elementIndex);

		notifyChanged();
	}

	public void invertCross(int x, int y) {
		if (!getElementByPos(x, y, elementIndex)) {
			return ;
		}

		ChipElement elem = model.elements[elementIndex.x][elementIndex.y];
		if (elem == null || elem.type != ChipElement.ELEMENT_WIRE) {
			return ;
		}

		if (elem.itemType == Drawer.WIRE_CROSS) {
			updateWireTypeByNeighbours(elementIndex);
		} else if (elem.itemType == Drawer.WIRE_FULL) {
			elem.itemType = Drawer.WIRE_CROSS;
		}

		notifyChanged();
	}

	private static final class NeighbourInfo {
		public final Point index = new Point();
		public ChipElement elem;
		public EditorItem  item;

		public boolean isSimpleWire() {
			return elem.type == ChipElement.ELEMENT_WIRE && elem.itemType != Drawer.WIRE_CROSS;
		}
	}
	private final NeighbourInfo neighbourInfo = new NeighbourInfo();

	private boolean getNeighbourInfo(Point index, int dir) {
        return getNeighbourInfo(
                index.x + Orientation.DX[dir],
                index.y + Orientation.DY[dir]);
	}

    private boolean getNeighbourInfo(int x, int y, int dir) {
        return getNeighbourInfo(
                x + Orientation.DX[dir],
                y + Orientation.DY[dir]);
    }


    private boolean getNeighbourInfo(int x, int y) {
        neighbourInfo.index.x = x;
        if (neighbourInfo.index.x < 0 || neighbourInfo.index.x >= items.length) {
            return false;
        }

        neighbourInfo.index.y = y;
        if (neighbourInfo.index.y < 0 || neighbourInfo.index.y >= items.length) {
            return false;
        }

        neighbourInfo.item = items[neighbourInfo.index.x][neighbourInfo.index.y];
        neighbourInfo.elem = model.elements[neighbourInfo.index.x][neighbourInfo.index.y];

        return neighbourInfo.item != null;
    }

	private void updateWireTypeByNeighbours(Point index) {
		EditorItem item = items[index.x][index.y];

		for (int k = 0; k < 4; ++k) {
			item.allow[k] = false;

			if (!getNeighbourInfo(index, k)) {
				continue;
			}

			item.allow[k] = neighbourInfo.item.allow[Orientation.invertDir(k)];
		}

		ChipElement elem = model.elements[index.x][index.y];
		elem.itemType = Drawer.getWireTypeByDirs(item.allow);
	}

	public void clearElement(int x, int y) {
		if (newElementMode) {
			return ;
		}

		if (!getElementByPos(x, y, elementIndex)) {
			return ;
		}

		removeElement(elementIndex, true);

		notifyChanged();
	}

    protected void removeElement(Point index, boolean removeSubsequent) {
        removeElement(index.x, index.y, removeSubsequent);
    }

	protected void removeElement(int x, int y, boolean removeSubsequent) {
		ChipElement elem = model.elements[x][y];
		if (elem == null) {
			return ;
		}

		if (elem.type == ChipElement.ELEMENT_INPUT) {
			model.releaseInput(elem.itemType);
		}
		if (elem.type == ChipElement.ELEMENT_OUTPUT) {
			model.releaseOutput(elem.itemType);
		}

		model.elements[x][y] = null;
		items[x][y] = null;

		for (int dir = 0; dir < 4; ++dir) {
			if (!getNeighbourInfo(x, y, dir)) {
				continue;
			}

			if (!neighbourInfo.isSimpleWire()) {
				continue;
			}

			neighbourInfo.item.allow[Orientation.invertDir(dir)] = false;
			neighbourInfo.elem.itemType = Drawer.getWireTypeByDirs(neighbourInfo.item.allow);
		}

		if (!removeSubsequent) {
			return ;
		}

		if (elem.type == ChipElement.ELEMENT_INPUT && getNeighbourInfo(x, y, Orientation.WEST)) {
			if (neighbourInfo.elem.type == ChipElement.ELEMENT_MARK) {
				removeElement(neighbourInfo.index, false);
			}
		}

		if (elem.type == ChipElement.ELEMENT_OUTPUT && getNeighbourInfo(x, y, Orientation.EAST)) {
			if (neighbourInfo.elem.type == ChipElement.ELEMENT_MARK) {
				removeElement(neighbourInfo.index, false);
			}
		}

		if (elem.type == ChipElement.ELEMENT_MARK && getNeighbourInfo(x, y, elem.orientation)) {
			removeElement(neighbourInfo.index, false);
		}

        if (elem.type == ChipElement.ELEMENT_CHIP_PART) {
            int partIndex = elem.orientation & Drawer.CHIP_INDEX_MASK;
            int count = elem.orientation >> Drawer.CHIP_COUNT_OFFSET;
            boolean isInput = Drawer.isInputChipPart(elem.orientation);

            int x0 = x - (isInput ? 0 : 1);
            int y0 = y - partIndex;

            for (int k = 0; k < 2; ++k) {
                for (int index = 0; index < count; ++index) {
                    getNeighbourInfo(x0 + k, y0 + index);
                    removeElement(neighbourInfo.index, false);
                }
            }

            model.removeModel(elem.itemType);
        }
	}

	private ChipElement createWire(int x, int y) {
		assert model.elements[x][y] == null;

		ChipElement elem = new ChipElement();
		elem.initWire();
		model.elements[x][y] = elem;

		items[x][y] = new EditorItem(elem);

		return elem;
	}

	private boolean newElementMode = false;
	private final Point newElementIndex = new Point();
	public boolean isNewElementMode() {
		return newElementMode;
	}

	public void startNewElementMode(int x, int y) {
		if (!getElementByPos(x, y, elementIndex)) {
			return ;
		}

		newElementMode = true;
		newElementIndex.setLocation(elementIndex);

		updateView();
	}

    public interface BrowseModelListener {
        ChipModel browse();
    }

    private BrowseModelListener browseModelListener;
    public void setBrowseModelListener(BrowseModelListener listener) {
        browseModelListener = listener;
    }

    public ChipModel browseModel() {
        if (browseModelListener == null) {
            return null;
        }

        return browseModelListener.browse();
    }

	public void chooseNewElement(int x, int y) {
		if (!newElementMode || !getElementByPos(x, y, elementIndex)) {
			return ;
		}

		elementsRange.setBounds(
			newElementIndex.x - newItemsOffset.x,
			newElementIndex.y - newItemsOffset.y,
			newItemsRange.width,
			newItemsRange.height
		);

		newElementMode = false;

		if (!elementsRange.contains(elementIndex)) {
			updateView();
			return ;
		}

		int newX = elementIndex.x - elementsRange.x;
		int newY = elementIndex.y - elementsRange.y;

        ChipElement toCreate = newItems[newX][newY];
        ChipElement newElem;

        if (toCreate.type == ChipElement.ELEMENT_CHIP_PART) {
            ChipModel newModel = browseModel();
            if (newModel == null) {
                updateView();
                return ;
            }

            newElem = new ChipElement();
            newElem.initChip(model.models.size(), newModel.getInputsCount(), 0, true);

            model.models.add(newModel);
        } else {
            newElem = toCreate.duplicate();
        }

		if (newElem.type == ChipElement.ELEMENT_INPUT) {
			newElem.itemType = model.acquireInput();

			if (newElementIndex.x == 0) {
				++newElementIndex.x;
			}
		}
		if (newElem.type == ChipElement.ELEMENT_OUTPUT) {
			newElem.itemType = model.acquireOutput();

			if (newElementIndex.y == model.elements.length - 1) {
				--newElementIndex.y;
			}
		}

		ChipElement elem = model.elements[newElementIndex.x][newElementIndex.y];
		if (elem != null && elem.type != ChipElement.ELEMENT_WIRE) {
			// if it's not a wire, use smart delete
			removeElement(newElementIndex, true);
		}
		model.elements[newElementIndex.x][newElementIndex.y] = newElem;

		EditorItem newItem = new EditorItem(newElem);
		items[newElementIndex.x][newElementIndex.y] = newItem;

		for (int k = 0; k < 4; ++k) {
			if (!getNeighbourInfo(newElementIndex, k)) {
				continue;
			}

			if (!neighbourInfo.isSimpleWire()) {
				continue;
			}

			if (!newItem.allow[k]) {
				neighbourInfo.item.allow[Orientation.invertDir(k)] = false;
				neighbourInfo.elem.itemType = Drawer.getWireTypeByDirs(neighbourInfo.item.allow);
			}
		}

        processNewElement(newElem, newElementIndex);

        notifyChanged();
    }

    private void processNewElement(ChipElement newElem, Point newElementIndex) {
        if (newElem.type == ChipElement.ELEMENT_KEY || newElem.type == ChipElement.ELEMENT_DIODE) {
            int dir = newElem.orientation;
            int invDir = Orientation.invertDir(dir);

            if (getNeighbourInfo(newElementIndex, dir) && neighbourInfo.isSimpleWire()) {
                neighbourInfo.item.allow[invDir] = true;
                neighbourInfo.elem.itemType = Drawer.getWireTypeByDirs(neighbourInfo.item.allow);
            }
            if (getNeighbourInfo(newElementIndex, invDir) && neighbourInfo.isSimpleWire()) {
                neighbourInfo.item.allow[dir] = true;
                neighbourInfo.elem.itemType = Drawer.getWireTypeByDirs(neighbourInfo.item.allow);
            }
        }

		// insert input & output markers
		if (newElem.type == ChipElement.ELEMENT_INPUT) {
            if (getNeighbourInfo(newElementIndex, Orientation.EAST) && neighbourInfo.isSimpleWire()) {
                neighbourInfo.item.allow[Orientation.WEST] = true;
                neighbourInfo.elem.itemType = Drawer.getWireTypeByDirs(neighbourInfo.item.allow);
            }

			--newElementIndex.x;
			removeElement(newElementIndex, true);

			newElem = new ChipElement();
			newElem.initMarker(Orientation.EAST);
			model.elements[newElementIndex.x][newElementIndex.y] = newElem;
			items[newElementIndex.x][newElementIndex.y] = new EditorItem(newElem);
		}

		if (newElem.type == ChipElement.ELEMENT_OUTPUT) {
            if (getNeighbourInfo(newElementIndex, Orientation.WEST) && neighbourInfo.isSimpleWire()) {
                neighbourInfo.item.allow[Orientation.EAST] = true;
                neighbourInfo.elem.itemType = Drawer.getWireTypeByDirs(neighbourInfo.item.allow);
            }

			++newElementIndex.x;
			removeElement(newElementIndex, true);

			newElem = new ChipElement();
			newElem.initMarker(Orientation.WEST);
			model.elements[newElementIndex.x][newElementIndex.y] = newElem;
			items[newElementIndex.x][newElementIndex.y] = new EditorItem(newElem);
		}

        if (newElem.type == ChipElement.ELEMENT_CHIP_PART) {
            int count = newElem.orientation >> Drawer.CHIP_COUNT_OFFSET;
            int x0 = newElementIndex.x;
            int y0 = newElementIndex.y;
            for (int k = 0; k < 2; ++k) {
                for (int index = 0; index < count; ++index) {
                    if (k == 0 && index == 0) {
                        continue;
                    }

                    newElementIndex.x = x0 + k;
                    newElementIndex.y = y0 + index;
                    removeElement(newElementIndex, true);

                    ChipElement part = newElem.duplicate();
                    part.orientation = Drawer.getChipOrientation(index, k == 0, count);
                    model.elements[newElementIndex.x][newElementIndex.y] = part;
                    items[newElementIndex.x][newElementIndex.y] = new EditorItem(part);
                }
            }
        }
	}

	private static final Rectangle newItemsRange = new Rectangle(0, 0, 4, 3);
	private static final Point newItemsOffset = new Point(1, 1);
	private final ChipElement[][] newItems;

	@Override
	public void draw() {
		super.draw();

		if (!newElementMode) {
			return ;
		}

		int itemSize = drawer.getItemSize();

		int columns = newItems.length;
		int rows    = newItems[0].length;

		int gridX = gridOffset.x + (newElementIndex.x - newItemsOffset.x - elementsRange.x) * itemSize;
		int gridY = gridOffset.y + (newElementIndex.y - newItemsOffset.y - elementsRange.y) * itemSize;

		drawer.pushTransform(gridX, gridY);

		drawer.setDrawPos(0, 0);
		drawer.drawEditGrid(columns, rows);

		drawElements(newItems, newItemsRange, itemSize);
		drawer.popTransform();
	}
}
